En este notebook se realiza el estudio y preprocesamiento de las variables numéricas y categoricas. Se realizarán los siguientes pasos:
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px
from sklearn.impute import KNNImputer
import scipy.stats as ss
import warnings
from sklearn.model_selection import train_test_split
warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 5000)
def plot_feature(df, col_name, isContinuous, target):
"""
Visualize a variable with and without faceting on the loan status.
- df dataframe
- col_name is the variable name in the dataframe
- full_name is the full variable name
- continuous is True if the variable is continuous, False otherwise
"""
f, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,3), dpi=90)
count_null = df[col_name].isnull().sum()
if isContinuous:
sns.histplot(df.loc[df[col_name].notnull(), col_name], kde=False, ax=ax1)
else:
sns.countplot(df, x=col_name, color='#5975A4', saturation=1, ax=ax1)
ax1.set_xlabel(col_name)
ax1.set_ylabel('Count')
ax1.set_title(col_name+ ' Numero de nulos: '+str(count_null))
plt.xticks(rotation = 90)
if isContinuous:
sns.boxplot(x=col_name, y=target, data=df, ax=ax2)
ax2.set_ylabel('')
ax2.set_title(col_name + ' by '+target)
else:
data = df.groupby(col_name)[target].value_counts(normalize=True).to_frame('proportion').reset_index()
data.columns = [i, target, 'proportion']
#sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
ax2.set_ylabel(target+' fraction')
ax2.set_title(target)
plt.xticks(rotation = 90)
ax2.set_xlabel(col_name)
plt.tight_layout()
def dame_variables_categoricas(dataset=None):
'''
----------------------------------------------------------------------------------------------------------
Función dame_variables_categoricas:
----------------------------------------------------------------------------------------------------------
-Descripción: Función que recibe un dataset y devuelve una lista con los nombres de las
variables categóricas
-Inputs:
-- dataset: Pandas dataframe que contiene los datos
-Return:
-- lista_variables_categoricas: lista con los nombres de las variables categóricas del
dataset de entrada con menos de 100 valores diferentes
-- 1: la ejecución es incorrecta
'''
if dataset is None:
print(u'\nFaltan argumentos por pasar a la función')
return 1
lista_variables_categoricas = []
other = []
for i in dataset.columns:
if (dataset[i].dtype!=float) & (dataset[i].dtype!=int):
unicos = int(len(np.unique(dataset[i].dropna(axis=0, how='all'))))
if unicos < 100:
lista_variables_categoricas.append(i)
else:
other.append(i)
return lista_variables_categoricas, other
def get_corr_matrix(dataset = None, metodo='pearson', size_figure=[10,8]):
# Para obtener la correlación de Spearman, sólo cambiar el metodo por 'spearman'
if dataset is None:
print(u'\nHace falta pasar argumentos a la función')
return 1
sns.set(style="white")
# Compute the correlation matrix
corr = dataset.corr(method=metodo)
# Set self-correlation to zero to avoid distraction
for i in range(corr.shape[0]):
corr.iloc[i, i] = 0
# Set up the matplotlib figure
f, ax = plt.subplots(figsize=size_figure)
# Draw the heatmap with the mask and correct aspect ratio
sns.heatmap(corr, center=0,
square=True, linewidths=.5, cmap ='viridis' ) #cbar_kws={"shrink": .5}
plt.show()
return 0
def get_deviation_of_mean_perc(pd_loan, list_var_continuous, target, multiplier):
"""
Devuelve el porcentaje de valores que exceden del intervalo de confianza
:type series:
:param multiplier:
:return:
"""
pd_final = pd.DataFrame()
for i in list_var_continuous:
series_mean = pd_loan[i].mean()
series_std = pd_loan[i].std()
std_amp = multiplier * series_std
left = series_mean - std_amp
right = series_mean + std_amp
size_s = pd_loan[i].size
perc_goods = pd_loan[i][(pd_loan[i] >= left) & (pd_loan[i] <= right)].size/size_s
perc_excess = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size/size_s
if perc_excess>0:
pd_concat_percent = pd.DataFrame(pd_loan[target][(pd_loan[i] < left) | (pd_loan[i] > right)]\
.value_counts(normalize=True).reset_index()).T
pd_concat_percent.columns = [pd_concat_percent.iloc[0,0],
pd_concat_percent.iloc[0,1]]
#print('las columnas son', pd_concat_percent.columns, pd_concat_percent)
pd_concat_percent = pd_concat_percent#.drop('index',axis=0)
pd_concat_percent['variable'] = i
pd_concat_percent['sum_outlier_values'] = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size
pd_concat_percent['porcentaje_sum_null_values'] = perc_excess
pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
if pd_final.empty:
print('No existen variables con valores nulos')
return pd_final
def get_percent_null_values_target(pd_loan, list_var_continuous, target):
pd_final = pd.DataFrame()
for i in list_var_continuous:
if pd_loan[i].isnull().sum()>0:
pd_concat_percent = pd.DataFrame(pd_loan[target][pd_loan[i].isnull()]\
.value_counts(normalize=True).reset_index()).T
pd_concat_percent.columns = [pd_concat_percent.iloc[0,0],
pd_concat_percent.iloc[0,1]]
pd_concat_percent = pd_concat_percent.drop('index',axis=0)
pd_concat_percent['variable'] = i
pd_concat_percent['sum_null_values'] = pd_loan[i].isnull().sum()
pd_concat_percent['porcentaje_sum_null_values'] = pd_loan[i].isnull().sum()/pd_loan.shape[0]
pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
if pd_final.empty:
print('No existen variables con valores nulos')
return pd_final
def cramers_v(confusion_matrix):
"""
calculate Cramers V statistic for categorial-categorial association.
uses correction from Bergsma and Wicher,
Journal of the Korean Statistical Society 42 (2013): 323-328
confusion_matrix: tabla creada con pd.crosstab()
"""
chi2 = ss.chi2_contingency(confusion_matrix)[0]
n = confusion_matrix.sum()
phi2 = chi2 / n
r, k = confusion_matrix.shape
phi2corr = max(0, phi2 - ((k-1)*(r-1))/(n-1))
rcorr = r - ((r-1)**2)/(n-1)
kcorr = k - ((k-1)**2)/(n-1)
return np.sqrt(phi2corr / min((kcorr-1), (rcorr-1)))
Lectura de los datos y cambio de tipos de variables, en esta parte del código vamos a leer los datos. Mediante la función shape vamos a observar cuales son las dimensiones del dataframe que vamos a analizar. En concreto, el data frame base, tiene 1 millón de filas y se compone de 32 columnas.
Gracias a la función columns podemos saber el nombre de cada una de las 32 columnas que componen a nuestro dataset.
path_folder = "./"
df_base = pd.read_csv(path_folder +"Base.csv", low_memory=False)
df_base.shape
(1000000, 32)
df_base.columns
Index(['fraud_bool', 'income', 'name_email_similarity',
'prev_address_months_count', 'current_address_months_count',
'customer_age', 'days_since_request', 'intended_balcon_amount',
'payment_type', 'zip_count_4w', 'velocity_6h', 'velocity_24h',
'velocity_4w', 'bank_branch_count_8w',
'date_of_birth_distinct_emails_4w', 'employment_status',
'credit_risk_score', 'email_is_free', 'housing_status',
'phone_home_valid', 'phone_mobile_valid', 'bank_months_count',
'has_other_cards', 'proposed_credit_limit', 'foreign_request', 'source',
'session_length_in_minutes', 'device_os', 'keep_alive_session',
'device_distinct_emails_8w', 'device_fraud_count', 'month'],
dtype='object')
Una de las funciones que se han creado para realizar este analisis es la funcion _dame_variablescategoricas. Esta función nos permite diferenciar dentro de un dataset si las variables son numéricas o categoricas. Esto es en base a si la variable tiene más de 100 valores distintos.
list_var_cat, other = dame_variables_categoricas(dataset=df_base)
df_base[list_var_cat] = df_base[list_var_cat].astype("category")
list_var_continuous = list(df_base.select_dtypes('float').columns)
df_base[list_var_continuous] = df_base[list_var_continuous].astype(float)
df_base.dtypes
fraud_bool category income float64 name_email_similarity float64 prev_address_months_count int64 current_address_months_count int64 customer_age category days_since_request float64 intended_balcon_amount float64 payment_type category zip_count_4w int64 velocity_6h float64 velocity_24h float64 velocity_4w float64 bank_branch_count_8w int64 date_of_birth_distinct_emails_4w category employment_status category credit_risk_score int64 email_is_free category housing_status category phone_home_valid category phone_mobile_valid category bank_months_count category has_other_cards category proposed_credit_limit float64 foreign_request category source category session_length_in_minutes float64 device_os category keep_alive_session category device_distinct_emails_8w category device_fraud_count category month category dtype: object
Como se ha mencionado en el 01_notebook nuestra variable objetivo tiene un distribución muy descompensada que después de realizar el análisis EDA convendría llevar a cabo un oversampling o undersampling.
pd_plot_fraud_status = df_base['fraud_bool']\
.value_counts(normalize=True)\
.mul(100).rename('percent').reset_index()
pd_plot_fraud_status_conteo = df_base['fraud_bool'].value_counts().reset_index()
pd_plot_fraud_status_pc = pd.merge(pd_plot_fraud_status,
pd_plot_fraud_status_conteo, left_index=True, right_index=True, how='inner')
fig = px.histogram(pd_plot_fraud_status_pc, x="fraud_bool_x", y=['percent'])
fig.show()
En esta parte del código estamos separando nuestro dataset en train y test para llevar a cabo el análisis de los mismos, de las variables, los null, etc.
X_fraud, X_fraud_test, y_fraud, y_fraud_test = train_test_split(df_base.drop('fraud_bool',axis=1),
df_base['fraud_bool'],
stratify=df_base['fraud_bool'],
test_size=0.2)
df_base_train = pd.concat([X_fraud, y_fraud],axis=1)
df_base_test = pd.concat([X_fraud_test, y_fraud_test],axis=1)
print('== Train\n', df_base_train['fraud_bool'].value_counts(normalize=True))
print('== Test\n', df_base_test['fraud_bool'].value_counts(normalize=True))
== Train fraud_bool 0 0.988971 1 0.011029 Name: proportion, dtype: float64 == Test fraud_bool 0 0.98897 1 0.01103 Name: proportion, dtype: float64
En este apartado hemos dividido el dataset en train y test. Vamos a usar train, que es la parte del dataset que utilizaremos para entrenar el modelo. Por otro lado la parte test, es la parte del dataframe que usaremos para validar el modelo y observar como se comporta. En este paso también se puede observar que los tanto el test como el train tienen la misma proporción de Fraud_bool=0 que Fraud_bool=1.
Veo el número de valores nulos por filas y por columnas
base_series_null_columns = df_base_train.isnull().sum().sort_values(ascending=False)
base_series_null_rows = df_base_train.isnull().sum(axis=1).sort_values(ascending=False)
print(base_series_null_columns.shape, base_series_null_rows.shape)
base_null_columnas = pd.DataFrame(base_series_null_columns, columns=['nulos_columnas'])
base_null_filas = pd.DataFrame(base_series_null_rows, columns=['nulos_filas'])
base_null_filas['target'] = df_base['fraud_bool'].copy()
base_null_columnas['porcentaje_columnas'] = base_null_columnas['nulos_columnas']/df_base_train.shape[0]
base_null_filas['porcentaje_filas']= base_null_filas['nulos_filas']/df_base_train.shape[1]
(32,) (800000,)
base_null_columnas
| nulos_columnas | porcentaje_columnas | |
|---|---|---|
| income | 0 | 0.0 |
| name_email_similarity | 0 | 0.0 |
| month | 0 | 0.0 |
| device_fraud_count | 0 | 0.0 |
| device_distinct_emails_8w | 0 | 0.0 |
| keep_alive_session | 0 | 0.0 |
| device_os | 0 | 0.0 |
| session_length_in_minutes | 0 | 0.0 |
| source | 0 | 0.0 |
| foreign_request | 0 | 0.0 |
| proposed_credit_limit | 0 | 0.0 |
| has_other_cards | 0 | 0.0 |
| bank_months_count | 0 | 0.0 |
| phone_mobile_valid | 0 | 0.0 |
| phone_home_valid | 0 | 0.0 |
| housing_status | 0 | 0.0 |
| email_is_free | 0 | 0.0 |
| credit_risk_score | 0 | 0.0 |
| employment_status | 0 | 0.0 |
| date_of_birth_distinct_emails_4w | 0 | 0.0 |
| bank_branch_count_8w | 0 | 0.0 |
| velocity_4w | 0 | 0.0 |
| velocity_24h | 0 | 0.0 |
| velocity_6h | 0 | 0.0 |
| zip_count_4w | 0 | 0.0 |
| payment_type | 0 | 0.0 |
| intended_balcon_amount | 0 | 0.0 |
| days_since_request | 0 | 0.0 |
| customer_age | 0 | 0.0 |
| current_address_months_count | 0 | 0.0 |
| prev_address_months_count | 0 | 0.0 |
| fraud_bool | 0 | 0.0 |
base_null_filas.head()
| nulos_filas | target | porcentaje_filas | |
|---|---|---|---|
| 944074 | 0 | 0 | 0.0 |
| 140149 | 0 | 0 | 0.0 |
| 974536 | 0 | 0 | 0.0 |
| 40493 | 0 | 0 | 0.0 |
| 240683 | 0 | 0 | 0.0 |
variables = ['prev_address_months_count', 'intended_balcon_amount', 'bank_months_count', 'session_length_in_minutes', 'device_distinct_emails_8w']
missing_count = []
missing_percentage = []
for variable in variables:
if variable == 'intended_balcon_amount':
missing_count.append(df_base_train[df_base_train[variable] < 0].shape[0])
else:
missing_count.append(df_base_train[df_base_train[variable] == -1].shape[0])
missing_percentage.append((missing_count[-1] / len(df_base_train)) * 100)
missing_data_df = pd.DataFrame({
'Variable': variables,
'Missing': missing_count,
'Porcentaje Missing': missing_percentage
})
missing_data_df
| Variable | Missing | Porcentaje Missing | |
|---|---|---|---|
| 0 | prev_address_months_count | 570465 | 71.308125 |
| 1 | intended_balcon_amount | 593785 | 74.223125 |
| 2 | bank_months_count | 202866 | 25.358250 |
| 3 | session_length_in_minutes | 1622 | 0.202750 |
| 4 | device_distinct_emails_8w | 296 | 0.037000 |
Como se puede observar vemos que ninguna fila o columna contiene valores NULL o 0. Pero en la leyenda del dataset se nos informa de que los nulos ya han sido previamente tratados por lo cual no hace falta realizar ese proceso. Como podemos observar más del 70% de las variables _prev_address_monthscount e _intended_balconamount.
for i in list(df_base_train.columns):
if (df_base_train[i].dtype==float) & (i!='fraud_bool'):
plot_feature(df_base_train, col_name=i, isContinuous=True, target='fraud_bool')
elif i!='fraud_bool':
plot_feature(df_base_train, col_name=i, isContinuous=False, target='fraud_bool')
Podemos observar que la gran mayoría de nuestros clientes tienen de 20 a 50 años. Después, en el gráfico en el que se relaciona la varible customer_age y fraud_bool, podemos observar que el customer_age de la mayoría de los fraud_bool=1 es mayor de 60. Teniendo un pequeño pico en los 80 años. Esto nos puede ayudar a crear un perfil con las características que tiene un cliente fraudulento. Estos gráficos no nos aportan más información por el problema antes mencionado, ya que hay muy pocos valores fraud_bool=1 en comparación con los fraud_bool=0.
A continuación, se tratan los valores missing, las correlaciones de las vairbales continuas y los outlier
list_var_continuous
Los valores outlier se pueden sustituir por la media, mediana, valores extremos (media+3std o media-3std). Tras el siguiente análisis, he decidido como primera iteración dejarlos sin sustituir. Una vez llegue al modelo puedo realizar iteraciones utilizando diferentes métodos para comprobar si mejora el modelo
get_deviation_of_mean_perc(df_base_train, list_var_continuous, target='fraud_bool', multiplier=3)
Los valores outlier son muy bajos por los que actualmente no vamos a tratarlos ya que no afectan al análisis del dataset.
get_corr_matrix(dataset = df_base_train[list_var_continuous],
metodo='pearson', size_figure=[10,8])
Como podemos observar las variables que más correlación tienen entre sí son variables que dependen unas de otras ya que dependen del factor tiempo pero indican lo mismo, en este caso mide la velocidad en la que se han hecho las solicitudes. Del resto de variables no podemos destacar nada. Sin tener en cuenta las variables mencionadas la siguiente que mayor correlación tienen es el proposed_credit_limit y el income. La correlacion que tienen es de 0.1. No es muy alta.
corr = df_base_train[list_var_continuous].corr('pearson')
new_corr = corr.abs()
new_corr.loc[:,:] = np.tril(new_corr, k=-1) # below main lower triangle of an array
new_corr = new_corr.stack().to_frame('correlation').reset_index().sort_values(by='correlation', ascending=False)
new_corr
Estas son las variables que tienen valores missing que estan anotados en la leyenda del dataset:
list_var_continuous
get_percent_null_values_target(df_base_train, list_var_continuous, target='fraud_bool')
Dentro de nuestro dataset no se han encontrado valores nulos, pero como antes hemos mencionado si existen. No estan como nulos si no que han querido reemplazarlos.
list_var_cat
confusion_matrix = pd.crosstab(df_base_train["fraud_bool"], df_base_train["customer_age"])
print(confusion_matrix)
cramers_v(confusion_matrix.values)
Si hacemos la correlación de una variable consigo misma sale una correlación del 1. Ya que son la misma variable por ende estan muy correlacionadas entre sí.
confusion_matrix = pd.crosstab(df_base_train["fraud_bool"], df_base_train["fraud_bool"])
cramers_v(confusion_matrix.values)
df_base_train.to_csv("./df_base_train.csv")
df_base_test.to_csv("./df_base_test.csv")
print(df_base_train.shape, df_base_test.shape)